package org.aj.web.exception;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.aj.base.exception.BusException;
import org.aj.base.web.response.ResponseResult;
import org.aj.web.exception.alarm.AlarmHandler;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.lang.Nullable;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;

/**
 * description  用于统一捕获服务器异常（例如400 405 500），定义统一的返回格式 ,方便给前端统一进行获取
 * @Author: 作者姓名 jcj
 * @CreateDate: 2019/3/31 13:39
 * @UpdateUser: aj
 * @UpdateDate: 2019/3/31 13:39
 * @UpdateRemark: The modified content
 * @Version: 1.0
 */
public class CustomCatchException extends DefaultHandlerExceptionResolver implements Ordered {





    //由调用者来决定使用哪个日志框架
    private static final Log log = LogFactory.getLog(CustomCatchException.class);



    private int order = Ordered.HIGHEST_PRECEDENCE +1;

    private  Environment environment;

    private CustomProperties customProperties;




    private WebContainerInitListener webContainerInitListener;

    private List<AlarmHandler> alarmHandlers = new ArrayList<>();

    public CustomCatchException(Environment environment, CustomProperties customProperties) {
        this.environment = environment;
        this.customProperties = customProperties;
    }



    public void setWebContainerInitListener(WebContainerInitListener webContainerInitListener) {
        this.webContainerInitListener = webContainerInitListener;
    }

    public void setAlarmHandlers(List<AlarmHandler> alarmHandlers) {
        this.alarmHandlers = alarmHandlers;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    @Override
    public int getOrder() {

        return order;
    }
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

        PrintWriter writer = null;
        try {

            ModelAndView modelAndView = doResolveException(request, response, handler, ex);

            if(null != modelAndView){
                return modelAndView;
            }

            if (ex instanceof ConstraintViolationException){
                return handleConstraintViolationException((ConstraintViolationException)ex,request,response,handler);
            }
            else {
                String requestURI = request.getRequestURI();
                if (ex instanceof BusException){
                    //设置字符集
                    response.setContentType("application/json;charset=UTF-8");
                    BusException busException = (BusException) ex;

                    //提取友好的提示语
                    String  msg =   getFriendlyMsg(busException);

                    Throwable throwable = busException.getThrowable();
                    if(null != throwable && !(throwable instanceof  BusException)){
                        //取出异常堆栈信息
                        String message = ExceptionUtils.getStackTrace(ex);
                        //包装错误信息
                        String detailMsg = warpErrorMsg(request, message);

                        //认为是系统有问题 故设置为500
                        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                        log.error("由BusException抛出业务异常,"+msg+"\n"+detailMsg);
                        if(!alarmHandlers.isEmpty()){
                            //设置邮件标题 目前格式为：应用名称+[加载的配置文件]+请求的URI+标题
                            String title = getAlarmTitle(requestURI);
                            for (AlarmHandler alarmHandler : alarmHandlers) {
                                if(alarmHandler.sendAlarmTimeIsRipe(requestURI)){
                                    alarmHandler.sendAlarm(detailMsg,title);
                                }
                            }
                        }
                    }
                    String s = JSON.toJSONString(ResponseResult.error(msg,busException.codeConvertToInteger()), SerializerFeature.WriteMapNullValue);
                    //响应给请求者
                    writer = response.getWriter();
                    writer.write(s);

                } else {

                    List<String> excludeExceptionList = customProperties.getExcludeExceptionList();
                    if(ObjectUtils.isNotEmpty(excludeExceptionList)){
                        for (String s : excludeExceptionList) {
                            if(StringUtils.isNotBlank(s)
                                    && !BusException.class.getName().equals(s)){
                                try {
                                    Class<?> aClass = Class.forName(s);
                                    if(aClass.isInstance(ex)){
                                        //表示交由下一个异常处理器处理
                                        return null;
                                    }
                                } catch (ClassNotFoundException e) {
                                    //没必要处理这里报错 证明是传入的包路径有误
                                }
                            }
                        }
                    }
                    response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                    //设置字符集
                    response.setContentType("application/json;charset=UTF-8");
                    //取出异常堆栈信息
                    String message = ExceptionUtils.getStackTrace(ex);

                    //包装错误信息
                    String detailMsg = warpErrorMsg(request, message);


                    log.error("请求接口异常:\n"+detailMsg);
                    String resMsg = message;
                    //判断是否需要采用友好提示语
                    boolean enableCustomMsgWhenServerError = customProperties.isEnableCustomMsgWhenServerError();
                    if(enableCustomMsgWhenServerError){
                        resMsg = getFriendlyMsg(ex);
                    }
                    if(!alarmHandlers.isEmpty()){
                        String title = getAlarmTitle(requestURI);
                        for (AlarmHandler alarmHandler : alarmHandlers) {
                            if(alarmHandler.sendAlarmTimeIsRipe(requestURI)){
                                alarmHandler.sendAlarm(detailMsg,title);
                            }
                        }
                    }
                    //响应给请求者

                    try {
                        writer = response.getWriter();
                    } catch (IllegalStateException e) {

                        if(response instanceof ResponseFacade){
                            //若其它地方已经抢先调用了response的读流方法 这里则重置 目前是通过try catch方式进行感应
                            ResponseFacade responseFacade = (ResponseFacade) response;
                            if(!responseFacade.isCommitted()){
                                responseFacade.reset();
                                response.setContentType("application/json;charset=UTF-8");
                                writer = response.getWriter();
                            }

                        }else{
                            log.error("读取流失败,\n"+e);
                        }

                    }
                    if(null != writer){
                        writer.write(JSON.toJSONString(ResponseResult.error(resMsg,ResponseResult.SERVER_ERROR_CODE), SerializerFeature.WriteMapNullValue));
                    }

                }
            }

        } catch (IOException e) {
            log.error("--在用response响应错误信息的时候报错---" + e.getMessage());

        } finally {
            if (writer != null) {
                writer.flush();
            }
        }

        return new ModelAndView();
    }

    private String getAlarmTitle(String requestURI) {
        //设置邮件标题 目前格式为：应用名称+[加载的配置文件]+请求的URI+标题
        String applicationName = environment.getProperty("spring.application.name");
        StringBuilder title = new StringBuilder();
        if(StringUtils.isNotBlank(applicationName)){
            title.append(applicationName);
        }
        //得到此时服务启动加载了哪些配置文件
        String[] activeProfiles = environment.getActiveProfiles();
        String activeProfile = "";
        if( ObjectUtils.isNotEmpty(activeProfiles)){
            //转换成字符串
            activeProfile = Arrays.toString(activeProfiles);
        }
        title.append(activeProfile).append(requestURI).append(customProperties.getMailTitle());
        return title.toString();
    }


    //包装错误信息
    private String warpErrorMsg(HttpServletRequest request, String message) {
        String reqParamsKey = customProperties.getReqParamsKey();
        Object attribute = request.getAttribute(reqParamsKey);

        if(null == attribute){
            attribute = request.getHeader(reqParamsKey);
            if(null != attribute){
                try {
                    attribute = URLDecoder.decode(attribute.toString(),customProperties.getDecodeReqHeadCharset());
                } catch (UnsupportedEncodingException e) {
                    log.error("在处理异常时利用URLDecoder解码请求头key为"+ reqParamsKey +" 携带的参数值失败:\n",e);

                }
            }

        }



        //拼接日志
        StringBuilder msgBuilder = new StringBuilder();

        //得到节点标识
        String property = System.getProperty(customProperties.getCloudNodeId());
        if(StringUtils.isBlank(property)){
            property = environment.getProperty(customProperties.getCloudNodeId());
            if(StringUtils.isBlank(property)){
                //在采用默认的获取方式 调用javaapi获取本机地址 以及tomcat端口号

                property=webContainerInitListener.getHostAndPort();
            }

        }
        //记录节点标识
        msgBuilder.append("节点名称:").append(property);
        msgBuilder.append("\n");

        //得到完整的请求路径这样更容易判断
        String requestURI = request.getRequestURL().toString();
        msgBuilder.append("请求路径:").append(requestURI);
        msgBuilder.append("\n");

        String remoteIp = getHttpRequestIP(request);
        if(StringUtils.isNotBlank(remoteIp)){
            msgBuilder.append("访问者ip:").append(remoteIp);
            msgBuilder.append("\n");
        }

        //得到调用者的令牌
        Map<String, Object> authMsg = getAuthMsg(request);

        if(!authMsg.isEmpty()){
            msgBuilder.append("调用者:").append(authMsg);
            msgBuilder.append("\n");
        }

        //记录请求方式
        msgBuilder.append("请求方式:").append(request.getMethod());
        msgBuilder.append("\n");

        //记录请求的媒体类型即 content-type
        msgBuilder.append("content-type:").append(request.getContentType());
        msgBuilder.append("\n");
        //记录耗时
        String requestStartTimeStr = request.getHeader(customProperties.getRequestStartTimeKey());
        if(StringUtils.isBlank(requestStartTimeStr)){
            requestStartTimeStr = request.getAttribute(customProperties.getRequestStartTimeKey())+"";
        }

        if(StringUtils.isNotBlank(requestStartTimeStr)){

            try {
                long requestStartTime = Long.parseLong(requestStartTimeStr);
                msgBuilder.append("耗时:").append(String.format("%.3f",(System.currentTimeMillis()-requestStartTime)*1.0/1000));
                msgBuilder.append("\n");
            } catch (NumberFormatException e) {
                //没必要处理
            }
        }

        if(null != attribute){

            msgBuilder.append("请求参数：").append(attribute);
            msgBuilder.append("\n");
        }
        //异常堆栈信息
        msgBuilder.append("堆栈信息:\n").append(message);
        //记录到日志
        return msgBuilder.toString();
    }



    /**
     * description 从请求头里面获取请求参数里面获取授权信息  若没有则返回一个空map
     * @author aj
     * date 2022/8/7 16:13
     * @param request
     * @return java.util.Map<java.lang.String,java.lang.Object>
     */
    private Map<String,Object>  getAuthMsg(HttpServletRequest request) {
        Map<String,Object> authHead=new HashMap<>();
        String[] authorizationKey = customProperties.getAuthorizationKey();
        if(ObjectUtils.isEmpty(authorizationKey)){
            return authHead;
        }
        for (String s : authorizationKey) {

            String header = request.getHeader(s);
            if(StringUtils.isBlank(header)){
                header = request.getParameter(s);
            }
            if(StringUtils.isNotBlank(header)){
                authHead.put(s,header);
            }
        }
        return authHead;
    }


    /**
     * 以流的方式直接写出而不是直接返回视图
     * @param ex
     * @param response
     * @param httpStatusCode
     * @throws IOException
     */
    private void writeErrorMsgByStream(Exception ex,
                                       HttpServletResponse response, Integer httpStatusCode) throws IOException {
        //设置字符集
        response.setContentType("application/json;charset=UTF-8");
        //设置请求状态码 保持跟原来框架一致
        response.setStatus(httpStatusCode);
        PrintWriter writer = response.getWriter();
        writer.write(JSON.toJSONString(ResponseResult.error(ex.getMessage(),httpStatusCode), SerializerFeature.WriteMapNullValue));
        writer.flush();
    }

    /**
     * 以流的方式直接写出而不是直接返回视图
     * @param errorMsg 自定义错误信息
     * @param response
     * @param httpStatusCode
     * @throws IOException
     */
    private void writeErrorMsgByStream(String errorMsg,
                                       HttpServletResponse response, Integer httpStatusCode) throws IOException {
        //设置字符集
        response.setContentType("application/json;charset=UTF-8");
        //设置请求状态码 保持跟原来框架一致
        response.setStatus(httpStatusCode);
        PrintWriter writer = response.getWriter();
        writer.write(JSON.toJSONString(ResponseResult.error(errorMsg,httpStatusCode), SerializerFeature.WriteMapNullValue));
        writer.flush();
    }





    /**
     * Handle the case where an {@linkplain org.springframework.web.bind.annotation.ModelAttribute @ModelAttribute} method
     * argument has binding or validation errors and is not followed by another
     * method argument of type {@link BindingResult}.
     * <p>By default, an HTTP 400 error is sent back to the client.
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler the executed handler
     * @return an empty ModelAndView indicating the exception was handled
     * @throws IOException potentially thrown from {@link HttpServletResponse#sendError}
     */
    protected ModelAndView handleBindException(BindException ex, HttpServletRequest request,
                                               HttpServletResponse response, @Nullable Object handler) throws IOException {

        //response.sendError(HttpServletResponse.SC_BAD_REQUEST);

        BindingResult bindingResult = ex.getBindingResult();
        String msg = getErrorMsgByBindResult(bindingResult);

        if(!StringUtils.isEmpty(msg)){
            writeErrorMsgByStream(msg,response,HttpServletResponse.SC_BAD_REQUEST);
        }else{
            writeErrorMsgByStream(ex,response, HttpServletResponse.SC_BAD_REQUEST);
        }


        return new ModelAndView();
    }

    private String getErrorMsgByBindResult(BindingResult bindingResult) {
        List<ObjectError> allErrors = bindingResult.getAllErrors();
        StringBuilder sb  = new StringBuilder();
        //构造错误信息
        if(!allErrors.isEmpty()){
            for (ObjectError allError : allErrors) {
                sb.append(allError.getDefaultMessage()+" ");
            }

        }
        return sb.toString();
    }
    /**
     * description 处理异常
     * author aj
     * date 2022/8/13 16:28
     * @param ex
     * @param request
     * @param response
     * @param handler
     * @return org.springframework.web.servlet.ModelAndView
     */
    private ModelAndView handleConstraintViolationException(ConstraintViolationException ex, HttpServletRequest request, HttpServletResponse  response, Object handler) throws IOException {
        Set<ConstraintViolation<?>> violations = ex.getConstraintViolations();
        StringBuilder sb  = new StringBuilder();
        for (ConstraintViolation<?> violation : violations) {
            sb.append(violation.getMessage()+" ");
        }
        String msg = sb.toString();
        if(!StringUtils.isEmpty(msg)){
            writeErrorMsgByStream(msg,response,HttpServletResponse.SC_BAD_REQUEST);
        }else{
            writeErrorMsgByStream(ex,response, HttpServletResponse.SC_BAD_REQUEST);
        }


        return new ModelAndView();
    }


    /**
     * Invoked to send a server error. Sets the status to 500 and also sets the
     * request attribute "javax.servlet.error.exception" to the Exception.
     */
    protected void sendServerError(Exception ex, HttpServletRequest request, HttpServletResponse response)
            throws IOException {

       // response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        request.setAttribute("javax.servlet.error.exception", ex);

        writeErrorMsgByStream(ex,response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);


    }


    //得到请求者ip
    public static String getHttpRequestIP(HttpServletRequest req)  {

        String ip = req.getHeader("remote_request_address");
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = req.getHeader("x-forwarded-for");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = req.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = req.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = req.getRemoteAddr();
        }
        // 对于通过多个代理的情况，第一个IP为客户端真实IP,多个IP按照','分割
        // "***.***.***.***".length()
        if (ip != null && ip.length() > 15) {
            if (ip.indexOf(",") > 0) {
                ip = ip.substring(0, ip.indexOf(","));
            }
        }
        return ip;

    }



    /**
     * description 提取出友好的提示语
     * @author aj
     * date 2024/6/14 10:16
     * @param throwable
     * @return java.lang.String
     */
    private String getFriendlyMsg(Throwable throwable) {
        List<String> useCustom500MsgExceptionList = customProperties.getUseCustom500MsgExceptionList();
        if(!useCustom500MsgExceptionList.isEmpty() && !(throwable instanceof  BusException)){
            boolean anyMatch = useCustom500MsgExceptionList.stream().anyMatch(s -> s.equals(throwable.getClass().getName()));
            if(anyMatch){
                return customProperties.getCustom500Msg();
            }
        }

        Integer truncateStackTraceLength = customProperties.getTruncateStackTraceLength();
        if(throwable instanceof BusException){
            BusException busException =(BusException) throwable;
            Throwable innerThrowable = busException.getThrowable();

            //若包裹的异常类型刚好跟采用友好默认提示语异常列表匹配则直接返回友好默认提示语
            if(!useCustom500MsgExceptionList.isEmpty() && null != innerThrowable && !(innerThrowable instanceof  BusException)){
                boolean anyMatch = useCustom500MsgExceptionList.stream().anyMatch(s -> s.equals(innerThrowable.getClass().getName()));
                if(anyMatch){
                    return customProperties.getCustom500Msg();
                }
            }

            String errorMsg = busException.getErrorMsg();
            if(StringUtils.isNotBlank(errorMsg)){
                return wrapErrorMsgUseCauseBy(throwable,errorMsg);
            }
            String busMsg = StringUtils.isBlank(busException.getMessage())?"":busException.getMessage();
            //截取下长度，避免出现开发人员直接调用 BusException(e)构造方法时碰巧包裹的异常类重写了Throwable类的toString方法是输出整个堆栈信息的
            if(StringUtils.isNotBlank(busMsg) && busMsg.length() > truncateStackTraceLength
                    && null != innerThrowable && !(innerThrowable instanceof BusException)){
                busMsg = busMsg.substring(0, truncateStackTraceLength);
            }
            return wrapErrorMsgUseCauseBy(throwable,busMsg);
        }

        String message = throwable.getMessage();
        if(StringUtils.isNotBlank(message)){
            return wrapErrorMsgUseCauseBy(throwable,message);
        }

        //若都没有提示语则直接提取堆栈里面的信息，默认取100个字符
        message = ExceptionUtils.getStackTrace(throwable);
        if(StringUtils.isBlank(message)){
            return customProperties.getCustom500Msg();
        }
        if(message.length() < truncateStackTraceLength){
            return message;
        }
        return message.substring(0, truncateStackTraceLength);

    }

    private String getFriendlyCauseMsg(Throwable throwable){
        Throwable rootThrowable = throwable;
        String causeMsg = "";
        Throwable latestCauseThrowable=null;
        Throwable nextThrowable;
        int index = 1;
        while (null != (nextThrowable =throwable.getCause())){
            if(index > customProperties.getTryGetExceptionCauseDepth()){
                break;
            }
            latestCauseThrowable = nextThrowable;
            throwable = nextThrowable;
            index++;
        }
        if(latestCauseThrowable == null){
            return causeMsg;
        }
        String message = latestCauseThrowable.getMessage();
        if(StringUtils.isBlank(message)){
            return causeMsg;
        }
        //这里做个优化若只有一层cause的时候，且没有手动传入message去构造异常类的
        //为了避免出现重复的语句例如：出现这样java.lang.ArithmeticException: / by zero: Caused by: java.lang.ArithmeticException: / by zero
        if(index == 2 && rootThrowable.getMessage().equals(rootThrowable.getCause().toString())){
            //返回空表示没必要返回causeBy 能这么判断的依据可以看java.lang.Throwable.Throwable(java.lang.Throwable)的构造过程
           return causeMsg;
        }
        StringBuilder sb = new StringBuilder("Caused by: ");
        //若出现多层嵌套业务异常类 则没必要打印类名
        if(!(throwable instanceof BusException) || !(latestCauseThrowable instanceof BusException)){
            String name = latestCauseThrowable.getClass().getName();
            sb.append(name).append(": ");
        }
        sb.append(message);
        return sb.toString();
    }

    private String wrapErrorMsgUseCauseBy(Throwable throwable,String errorMsg){

        //java.lang.ArithmeticException: / by zero

        String friendlyCauseMsg = getFriendlyCauseMsg(throwable);
        //包装返回的提示语格式为：简单类名: +errorMsg: +friendlyCauseMsg
        StringBuilder  sb =new StringBuilder();
        String shortClassName = ClassUtils.getShortClassName(throwable, "");
        sb.append(shortClassName).append(":")
                .append(StringUtils.SPACE);
        if(StringUtils.isNotBlank(errorMsg)){
            if(StringUtils.isBlank(friendlyCauseMsg)){
                sb.append(" Caused by: ");
            }
            sb.append(errorMsg);
            sb.append(StringUtils.SPACE);
        }
        if(StringUtils.isNotBlank(friendlyCauseMsg)){

            sb.append(friendlyCauseMsg);
        }
        String string = sb.toString();

        //在做一次优化 当开发人员在写异常处理的时候 进行了无用了多层嵌套的时候 例如会变成这样：
        //BusException:  Caused by: [包路径].BusException: BusException:  Caused by: [错误信息]
        String simpleName = BusException.class.getSimpleName()+":";
        String className = BusException.class.getName()+":";
        if(throwable instanceof BusException &&
                ( (StringUtils.isNotBlank(errorMsg) && errorMsg.contains(simpleName))||
                        ( StringUtils.isNotBlank(friendlyCauseMsg) && friendlyCauseMsg.contains(simpleName))
                ) ){
            string=  string.replaceAll( className,"")
                    .replaceAll(simpleName,"")
                    .replaceAll("Caused by:","").trim();
            StringBuilder sbNew = new StringBuilder(simpleName);
            sbNew.append(StringUtils.SPACE).append("Caused by: ").append(string);
            return sbNew.toString();
        }
        return string;
    }
}
